<?php
include('headers.inc.php');
/**
* @package phpMRIMPro
* @version 1.0 (10)
* @author San4oP
* @copyright 2009 San4oP
* @link http://pmp.san4op.ru/forum/index.php
*/

class mrim
{
    const VERSION = '1.0';
    const BUILD = '10';

    /**
    * Socket resourse variable
    * 
    * @access public
    * @var resourse
    */
    public $sock = null;

    /**
    * Last error string
    * 
    * @access public
    * @var string
    */
    public $error = '';

    /**
    * Dubug mode
    * 
    * @access public
    * @var bool
    */
    public $debug = false;

    /**
    * Sequence
    * Счетчик пакетов S <-> C
    * 
    * @access private
    * @var int
    */
    private $seq = 0;

    /**
    * Список групп
    * 
    * @access private
    * @var array
    */
    private $groups = array();

    /**
    * Список контактов
    * 
    * @access public
    * @var array
    */
    public $contacts = array();

    /**
    * Ping parameters
    * 
    * @access private
    * @var array
    */
    private $ping = array();

    /**
    * Statuses
    * 
    * @access private
    * @var string
    */
    private $status = null;
    private $status_uri = null;
    private $status_title = null;
    private $status_descr = null;

    /**
    * Client name
    * 
    * @access private
    * @var string
    */
    private $client_name = null;

    /**
    * User agent
    * 
    * @access private
    * @var string
    */
    private $user_agent = null;

    /**
    * Statuses list
    * 
    * @access private
    * @var array
    */
    private $statuses = array(
        0x00000000 => 'offline',
        0x00000001 => 'online',
        0x00000002 => 'away',
        0x00000003 => 'notviewed',
        0x00000004 => 'other',
        0x80000000 => 'invisible',
    );

    /**
    * Расширенные статусы
    * 
    * @access private
    * @var array
    */
    private $substatuses = array(
        'STATUS_ONLINE'    => array('status' => 'online', 'title' => 'В сети'),
        'STATUS_AWAY'      => array('status' => 'away', 'title' => 'Отошел'),
        'STATUS_INVISIBLE' => array('status' => 'invisible', 'title' => 'Невидимый'),
        'status_chat'      => array('status' => 'other', 'title' => 'Готов поболтать'),
        'status_dnd'       => array('status' => 'other', 'title' => 'Не беспокоить'),
        'status_0'         => array('status' => 'offline', 'title' => 'Отключен'),
        'status_1'         => array('status' => 'online', 'title' => 'В сети'),
        'status_2'         => array('status' => 'away', 'title' => 'Отошел'),
        'status_3'         => array('status' => 'invisible', 'title' => 'Невидимый'),
        'status_4'         => array('status' => 'other', 'title' => 'Болею'),
        'status_5'         => array('status' => 'other', 'title' => 'Дома'),
        'status_6'         => array('status' => 'other', 'title' => 'Кушаю'),
        'status_7'         => array('status' => 'other', 'title' => 'Где я?'),
        'status_8'         => array('status' => 'other', 'title' => 'В туалете'),
        'status_9'         => array('status' => 'other', 'title' => 'Готовлю'),
        'status_10'        => array('status' => 'other', 'title' => 'Гуляю'),
        'status_11'        => array('status' => 'other', 'title' => 'Я Инопланетный разум!'),
        'status_12'        => array('status' => 'other', 'title' => 'Йа креветко!'),
        'status_13'        => array('status' => 'other', 'title' => 'Я потерялся!'),
        'status_14'        => array('status' => 'other', 'title' => 'Я сошел с ума!'),
        'status_15'        => array('status' => 'other', 'title' => 'Йа утко!'),
        'status_16'        => array('status' => 'other', 'title' => 'Играю'),
        'status_17'        => array('status' => 'other', 'title' => 'Курю'),
        'status_18'        => array('status' => 'other', 'title' => 'На работе'),
        'status_19'        => array('status' => 'other', 'title' => 'На встрече'),
        'status_20'        => array('status' => 'other', 'title' => 'Пью пиво'),
        'status_21'        => array('status' => 'other', 'title' => 'Пью кофе'),
        'status_22'        => array('status' => 'other', 'title' => 'Работаю'),
        'status_23'        => array('status' => 'other', 'title' => 'Сплю'),
        'status_24'        => array('status' => 'other', 'title' => 'Телефон'),
        'status_26'        => array('status' => 'other', 'title' => 'В институте'),
        'status_27'        => array('status' => 'other', 'title' => 'В школе'),
        'status_28'        => array('status' => 'other', 'title' => 'Вы ошиблись номером'),
        'status_29'        => array('status' => 'other', 'title' => ':-D'),
        'status_30'        => array('status' => 'other', 'title' => ';-)'),
        'status_32'        => array('status' => 'other', 'title' => '}:)'),
        'status_33'        => array('status' => 'other', 'title' => '8-)'),
        'status_34'        => array('status' => 'other', 'title' => ':-('),
        'status_35'        => array('status' => 'other', 'title' => ':\'('),
        'status_36'        => array('status' => 'other', 'title' => 'В шоке...'),
        'status_37'        => array('status' => 'other', 'title' => 'Злюсь!'),
        'status_38'        => array('status' => 'other', 'title' => 'Диабло!'),
        'status_39'        => array('status' => 'other', 'title' => '(_|_)'),
        'status_40'        => array('status' => 'other', 'title' => 'Сердце'),
        'status_41'        => array('status' => 'other', 'title' => 'Сплю'),
        'status_42'        => array('status' => 'other', 'title' => 'Cool!'),
        'status_43'        => array('status' => 'other', 'title' => 'Yo!'),
        'status_44'        => array('status' => 'other', 'title' => 'Фига'),
        'status_45'        => array('status' => 'other', 'title' => 'Fuck!'),
        'status_46'        => array('status' => 'other', 'title' => 'Череп'),
        'status_47'        => array('status' => 'other', 'title' => 'Ракета'),
        'status_48'        => array('status' => 'other', 'title' => 'Осминог'),
        'status_49'        => array('status' => 'other', 'title' => 'Коза'),
        'status_50'        => array('status' => 'other', 'title' => 'Bad!'),
        'status_51'        => array('status' => 'other', 'title' => 'Белка'),
        'status_52'        => array('status' => 'other', 'title' => 'Звезда'),
        'status_53'        => array('status' => 'other', 'title' => 'Музыка')
    );






    /**
    * Иницилизация класса
    * 
    * @param array $args
    * @return mrim
    */
    function __construct($args)
    {
        $keys = array(
            'status_uri'   => 'STATUS_ONLINE',
            'status_title' => 'В сети',
            'status_descr' => ''
        );

        foreach($keys as $key => $val) {
            if(isset($args[$key]) && $args[$key] != '') {
                if($key == 'status_uri') {
                    if(isset($this->substatuses[$args['status_uri']])) $this->status_uri = $args['status_uri'];
                    else $this->status_uri = $val;
                }
                elseif($key == 'status_title') {
                    if(strlen($args['status_title']) > 16) $this->status_title = substr($args['status_title'], 0, 13).'...';
                    else $this->status_title = $args['status_title'];
                }
                elseif($key == 'status_descr') {
                    if(strlen($args['status_descr']) > 255) $this->status_descr = substr($args['status_descr'], 0, 252).'...';
                    else $this->status_descr = $args['status_descr'];
                }
                else $this->$key = $args[$key];
            }
            else $this->$key = $val;
        }

        $this->user_agent = 'client="phpMRIM Pro" version="'.mrim::VERSION.'" build="'.mrim::BUILD.'"';
        $this->client_name = 'phpMRIM Pro '.mrim::VERSION.' ('.mrim::BUILD.')';
    }


    /**
    * Указание своего клиента
    * 
    * @param string $useragent
    * @param string $lang
    * @param string $version
    * @param string $build
    * @return bool
    */
    public function setUserAgent($useragent = '', $lang = 'ru', $version = '', $build = '')
    {
        if(!$useragent) return false;

        $lang = ($lang != '' && strlen($lang) == 2) ? $lang : 'ru';
        $version = $version != '' ? $version : mrim::VERSION;
        $build = $build != '' ? $build : mrim::BUILD;

        $this->user_agent = 'client="'.$useragent.'" version="'.$version.'" build="'.$build.'"';
        $this->client_name = $useragent.' '.$version.' ('.$build.')';

        return true;
    }


    /**
    * Проверка Email-адреса
    * 
    * @param string $email
    * @return bool
    */
    public function checkEmail(&$email)
    {
        if(!($email = trim($email))) return false;
        $name = reset(explode('@', $email));
        $first = substr($name, 0, 1);
        $last  = substr($name, -1);
        if($first == '-'||$first == '_'||$first == '.'||$last == '.'||!preg_match("~^[a-z0-9_\.\-]{1,16}@((corp\.)?mail|list|inbox|bk)\.ru$~is",$email))
            return false;

        return true;
    }


    /**
    * Записаваем дамп данных в файл
    * 
    * @access public
    * @param string $title
    * @param string $data
    * @param bool $hex
    * @param string $filename
    * @return void
    */
    public function dump($title, $data, $hex = true, $filename = 'dump')
    {
        if(!$this->debug) return;
        if($hex) {
            $results = str_split(chunk_split(chunk_split(strtoupper(bin2hex($data)), 2, ' '), 24, ' '), 50);
            foreach($results as $key => $line) {
                $results[$key] = str_pad($line, 50) . str_replace(array("\r", "\n", "\t"), array(".", ".", "."), substr($data, 0+16*$key, 16));
            }
            $data = implode("\r\n", $results);
        }

        $f = fopen($filename, 'a');
        fwrite($f, date('d.m.y H:i:s')."\n".$title."\n".trim($data)."\n\n\n");
        fclose($f);
    }


    /**
    * Пакуем данные в пакет
    * 
    * magic - Magic
    * proto - Версия протокола
    * seq - Sequence
    * msg - Тип пакета
    * dlen - Длина данных
    * from - Адрес отправителя
    * fromport - Порт отправителя
    * reserved[16] - Зарезервировано
    * 
    * @access private
    * @param mixed $type
    * @param mixed $data
    * @return string
    */
    private function packPacket($type, $data = '')
    {
        $dlen = (int) strlen($data);
        $packet = pack('L11', CS_MAGIC, PROTO_VERSION, $this->seq, $type, $dlen, 0, 0, 0, 0, 0, 0);
        if($dlen > 0) $packet .= $data;
        $this->seq++;
        return $packet;
    }


    /**
    * Преобразуем строку в формат LPS
    * 
    * @access private
    * @param string $string
    * @return string
    */
    private function str2lps($string)
    {
        return pack('L1', strlen($string)).$string;
    }


    /**
    * Преобразуем строку из LPS в текстовый формат
    * 
    * @access private
    * @param string $string
    * @return string
    */
    private function lps2str(&$string, $skip = 0)
    {
        if(!$string) return false;

        if($skip > 0) $string = substr($string, $skip);
        $len = unpack('L1', substr($string, 0, 4));
        $len = $len[1];
        if($len > 0) $result = substr($string, 4, $len); else $result = '';
        $string = substr($string, 4+$len);
        return $result;
    }


    /**
    * Преобразуем число в UL формат
    * 
    * @access private
    * @param int $string
    * @return string
    */
    private function int2ul($data)
    {
        return pack('L1', $data);
    }


    /**
    * Декодируем UL в нормальное число
    * 
    * @access private
    * @param string $string
    * @return int
    */
    private function ul2int(&$string, $skip = 0)
    {
        if(!$string) return false;

        if($skip > 0) $string = substr($string, $skip);
        $data = unpack('L1', substr($string, 0, 4));
        $data = $data[1];
        $string = substr($string, 4);
        return $data;
    }


    /**
    * Перекодировка строки из UTF-16LE в Windows-1251
    * 
    * @param string $string
    * @return string
    */
    private function unicode2win($string)
    {
        if(is_string($string) && (strstr($string, "\x00") || strstr($string, "\x04")))
            $string = iconv('utf-16le', 'cp1251//TRANSLIT', $string);

        return $string;
    }


    /**
    * Перекодировка строки из Windows-1251 в UTF-16LE
    * 
    * @param string $string
    * @return string
    */
    private function win2unicode($string)
    {
        if(is_string($string) && !strstr($string, "\x00") && !strstr($string, "\x04"))
            $string = iconv('cp1251', 'utf-16le//TRANSLIT', $string);

        return $string;
    }


    /**
    * Преобразуем RTF в простой текст
    * 
    * @access private
    * @param string $string
    * @return string
    */
    private function rtf2str($string)
    {
        if(!$string) return null;

        $string = str_replace(array("\r", '\\\line', '\line'), array('', "\n", "\n"), $string);
        $string = preg_replace('~(\\\[a-z0-9*]+({[^}]*}){0,1}([ a-z0-9]*;{1}){0,1})~si', '', $string);
        $string = str_replace(array('{', '}'), '', $string);
        return preg_replace_callback("~\\\'([0-9a-f]{2})~", array('mrim', 'convert'), trim($string));

        return $string;
    }


    /**
    * Преобразуем HEX в символ
    * 
    * @access private
    * @param string $symbol
    * @return string
    */
    private function convert($symbol)
    {
        return chr(hexdec($symbol[1]));
    }


    /**
    * Преобразуем сообщение в RTF формат
    * 
    * @access private
    * @param string $string
    * @return string
    */
    private function str2rtf($string)
    {
        if(!$string) return null;

        $new_string = '{\\rtf1\\ansi\\ansicpg0\\uc1\\deff0\\deflang0\\deflangfe0\\ltrdoc{\\fonttbl{\\f0\\fnil Tahoma;}}{\\colortbl;\\red0\\green0\\blue0;\\red0\\green0\\blue255;\\red0\\green255\\blue255;\\red0\\green255\\blue0;\\red255\\green0\\blue255;\\red255\\green0\\blue0;\\red255\\green255\\blue0;\\red255\\green255\\blue255;\\red0\\green0\\blue128;\\red0\\green128\\blue128;\\red0\\green128\\blue0;\\red128\\green0\\blue128;\\red128\\green0\\blue0;\\red128\\green128\\blue0;\\red128\\green128\\blue128;\\red192\\green192\\blue192;}'."\r\n\r\n".'\\uc0'."\r\n".'\\pard\\fi0\\li0\\ql\\ri0\\sb0\\sa0\\ltrpar\\itap0 \\plain \\f0\\ltrch\\fs20 ';

        $string = str_replace("\r", '', $string);
        for($i=0,$a=strlen($string);$i<$a;$i++) {
            $ch = $string[$i];
            switch($ch)
            {
                case "\t":
                    $new_string .= '\\tab ';
                break;
                
                case '\\':
                    $new_string .= '\\\\';
                break;
                
                case '{':
                    $new_string .= '\\{';
                break;
                
                case '}':
                    $new_string .= '\\}';
                break;
                
                case "\n":
                    $new_string .= '\\line ';
                break;

                default:
                    $new_string .= $ch;
                break;
            }
        }

        return $new_string.'\\par}';
    }


    /**
    * Создаем RTF форматированное сообщение
    * 
    * @access private
    * @param string $string
    * @return string
    */
    private function createRtfMessage($string)
    {
        if(!$string) return null;

        $packet = $this->int2ul(0x00000002) . $this->str2lps( $this->str2rtf($string) ) . $this->str2lps( $this->int2ul(0x00ffffff) );
        $packet = base64_encode(gzcompress($packet, 9));

        return $packet;
    }


    /**
    * Определяем тип пакета и преобразуем данные в массив
    * 
    * @access private
    * @param mixed $type
    * @param string $data
    * @return array
    */
    private function parsePacket($seq, $type, $data, $headers = '')
    {
        if(!$type) return false;

        switch($type)
        {
            /**
            * MRIM_CS_HELLO_ACK
            * Подтверждение установки соединения.
            */
            case MRIM_CS_HELLO_ACK:
                if($this->debug) $this->dump('[<<<] MRIM_CS_HELLO_ACK:', $headers.$data);

                $data = $this->ul2int($data)-1;
                $this->ping['interval'] = $data;

                $data = array('type' => 'hello', 'ping_period' => $data);
            break;

            /**
            * MRIM_CS_LOGIN_ACK
            * Авторизация успеша.
            */
            case MRIM_CS_LOGIN_ACK:
                if($this->debug) $this->dump('[<<<] MRIM_CS_LOGIN_ACK:', $headers.$data);

                $data = array('type' => 'login', 'status' => true);
            break;

            /**
            * MRIM_CS_LOGIN_REJ
            * Неверная авторизация.
            */
            case MRIM_CS_LOGIN_REJ:
                if($this->debug) $this->dump('[<<<] MRIM_CS_LOGIN_REJ:', $headers.$data);

                $data = $this->lps2str($data);
                $data = array('type' => 'login', 'status' => false, 'reason' => $data);
            break;

            /**
            * MRIM_CS_MESSAGE_ACK
            * Доставка сообщения
            */
            case MRIM_CS_MESSAGE_ACK:
                if($this->debug) $this->dump('[<<<] MRIM_CS_MESSAGE_ACK:', $headers.$data);

                $msg_id = $this->ul2int($data);
                $flag   = $this->ul2int($data);
                $from   = $this->lps2str($data);
                $msg    = $this->lps2str($data);

                if($flag != MESSAGE_FLAG_NORECV && $from != false && $msg_id != false) {
                    $packet = $this->str2lps($from) . $this->int2ul($msg_id);
                    $this->writeSocket($this->packPacket(MRIM_CS_MESSAGE_RECV, $packet), 'MRIM_CS_MESSAGE_RECV');
                    unset($packet);
                }

                if($flag == MESSAGE_FLAG_CONTACT) // contact
                    $data = false;

                elseif($flag == 0x10000c) // authorization
                {
                    $msg  = base64_decode($msg);
                    $name = $this->unicode2win($this->lps2str($msg, 4));
                    $msg  = $this->unicode2win($this->lps2str($msg));

                    $data = array(
                        'type'    => 'auth',
                        'from'    => $from,
                        'name'    => $name,
                        'message' => $msg
                    );
                }
                elseif($flag == 0x300404) // typing
                {
                    $data = array(
                        'type'    => 'typing',
                        'from'    => $from
                    );
                }
                else // other
                {
                    $msg = $this->unicode2win($msg);

                    if($msg == '' && strlen($data) > 0) {
                        $msg = $this->lps2str($data);
                        if($msg != '') $msg = $this->rtf2str( substr( gzuncompress( base64_decode( $msg ) ), 8, -8) );
                        else $msg = '';
                    }

                    $data = array(
                        'type'    => 'message',
                        'msg_id'  => $msg_id,
                        'flag'    => $flag,
                        'from'    => $from,
                        'message' => $msg,
                    );
                }
            break;

            /**
            * MRIM_CS_MESSAGE_STATUS
            * Подтверждение получения сообщения
            */
            case MRIM_CS_MESSAGE_STATUS:
                if($this->debug) $this->dump('[<<<] MRIM_CS_MESSAGE_STATUS:', $headers.$data);

                $status = $this->ul2int($data);

                if($status == MESSAGE_DELIVERED) $status = 'ok';
                elseif($status == MESSAGE_REJECTED_INTERR) $status = 'interr';
                elseif($status == MESSAGE_REJECTED_NOUSER) $status = 'nouser';
                elseif($status == MESSAGE_REJECTED_LIMIT_EXCEEDED) $status = 'limited';
                elseif($status == MESSAGE_REJECTED_TOO_LARGE) $status = 'toolarge';
                elseif($status == MESSAGE_REJECTED_DENY_OFFMSG) $status = 'denyoffmsg';
                else $status = 'unknown';

                $data = array('type' => 'accept', 'id' => $seq, 'status' => $status);
            break;

            /**
            * MRIM_CS_USER_STATUS
            * Смена статуса другого пользователя
            */
            case MRIM_CS_USER_STATUS:
                if($this->debug) $this->dump('[<<<] MRIM_CS_USER_STATUS:', $headers.$data);

                $status       = $this->ul2int($data);
                $status_uri   = $this->lps2str($data);
                $status_title = $this->unicode2win($this->lps2str($data));
                $status_descr = $this->unicode2win($this->lps2str($data));
                $user         = $this->lps2str($data);
                $unknown      = $this->ul2int($data);
                $client       = $this->lps2str($data);

                if( isset($this->statuses[ $status ]) )       $status = $this->statuses[ $status ];
                if( isset($this->substatuses[ $status_uri ]) ) $status_uri = $this->substatuses[ $status_uri ]['title'];

                $data = array(
                        'type' => 'userstatus',
                        'user' => $user,
                        'status' => $status,
                        'status_uri' => $status_uri,
                        'status_title' => $status_title,
                        'status_descr' => $status_descr,
                        'client' => $client,
                        'unknown' => $unknown
                );
            break;

            /**
            * MRIM_CS_LOGOUT
            * Закрытие соединения
            */
            case MRIM_CS_LOGOUT:
                if($this->debug) $this->dump('[<<<] MRIM_CS_LOGOUT:', $headers.$data);

                $norelogin = false;
                if(strlen($data) >= 4) if($this->ul2int($data) == LOGOUT_NO_RELOGIN_FLAG) $norelogin = true;

                $data = array('type' => 'logout', 'norelogin' => $norelogin);
            break;
            
            /**
            * MRIM_CS_CONNECTION_PARAMS
            * Изменение параметров соединения
            */
            case MRIM_CS_CONNECTION_PARAMS:
                if($this->debug) $this->dump('[<<<] MRIM_CS_CONNECTION_PARAMS:', $headers.$data);

                $new_ping = $this->ul2int($data)-1;
                $this->ping['interval'] = $new_ping;

                $data = array('type' => 'newping', 'period' => $new_ping);
            break;

            /**
            * MRIM_CS_OFFLINE_MESSAGE_ACK
            * Сообщение доставленное, пока пользователь не был подключен к сети
            */
            case MRIM_CS_OFFLINE_MESSAGE_ACK:
                if($this->debug) $this->dump('[<<<] MRIM_CS_OFFLINE_MESSAGE_ACK:', $headers.$data);

                $packet = $this->packPacket(MRIM_CS_DELETE_OFFLINE_MESSAGE, substr($data, 0, 8));
                $this->writeSocket($packet, 'MRIM_CS_DELETE_OFFLINE_MESSAGE');

                $msg = $this->lps2str($data, 8);

                preg_match_all("~^([a-zA-Z\-]+?): (.+?)[\r\n]$~ism", $msg, $headers);
                $headers[2][3] = end(explode('=', $headers[2][3]));
                preg_match("~--".$headers[2][3]."(.+?)--".$headers[2][3]."~ism", $msg, $matches);
                $message = trim($this->unicode2win(base64_decode(trim(preg_replace("~^([a-zA-Z\-]+?): (.+?)$~ism", '', $matches[1])))));

                foreach(array('From', 'Date', 'X-MRIM-Flags', 'X-MRIM-Version') as $search)
                {
                    $id = array_search($search, $headers[1]);
                    if($search == 'From') $from = trim($headers[2][ $id ]);
                    if($search == 'Date') $date = trim($headers[2][ $id ]);
                    if($search == 'X-MRIM-Flags') $flag = trim($headers[2][ $id ]);
                    if($search == 'X-MRIM-Version') $mrim_ver = trim($headers[2][ $id ]);
                }

                $data = array('type' => 'offline', 'date' => $date, 'flag' => $flag, 'mrim_ver' => $mrim_ver, 'from' => $from, 'message' => $message);
            break;

            /**
            * MRIM_CS_MAILBOX_STATUS
            * Количество писем в почтовом ящике
            */
            case MRIM_CS_MAILBOX_STATUS:
                if($this->debug) $this->dump('[<<<] MRIM_CS_MAILBOX_STATUS:', $headers.$data);

                $unread = $this->ul2int($data);;

                $data = array('type' => 'mailbox', 'unread' => $unread);
            break;

            /**
            * MRIM_CS_CONTACT_LIST
            * Контакт-лист
            */
            case MRIM_CS_CONTACT_LIST:
                if($this->debug) $this->dump('[<<<] MRIM_CS_CONTACT_LIST:', $headers.$data);

                $this->parseContacts($data);
                $data = array('type' => 'contactlist');
            break;

            /**
            * MRIM_CS_ANKETA_INFO
            * Найденные пользователи
            */
            case MRIM_CS_ANKETA_INFO:
                if($this->debug) $this->dump('[<<<] MRIM_CS_ANKETA_INFO:', $headers.$data);

                $status  = $this->ul2int($data);
                $results = false;
                $error   = false;

                if($status == MRIM_ANKETA_INFO_STATUS_OK)
                {
                    $fields_num  = $this->ul2int($data);
                    $max_rows    = $this->ul2int($data);
                    $server_time = $this->ul2int($data);

                    // get fields
                    for($i=1;$i<=$fields_num;$i++)
                    {
                        $field_name = $this->lps2str($data);
                        $results[$field_name] = null;
                    }

                    $utfencode = array('Nickname', 'FirstName', 'LastName', 'Location', 'status_title', 'status_desc');

                    // get values
                    reset($results);
                    while($field_name = key($results))
                    {
                        $field_value = $this->lps2str($data);

                        if(in_array($field_name, $utfencode)) $field_value = $this->unicode2win($field_value);
                        if($field_name == 'Sex') $field_value = $field_value == 1 ? 'male' : 'female';
                        if($field_name == 'status_uri' && isset($this->substatuses[$field_value])) $field_value = $this->substatuses[$field_value]['title'];
                        if($field_name == 'mrim_status' && isset($this->statuses[ intval($field_value) ]))
                            $field_value = $this->statuses[ intval($field_value) ];

                        $results[$field_name] = $field_value;
                        next($results);
                    }
                }
                elseif($status == MRIM_ANKETA_INFO_STATUS_NOUSER) $error = 'nousers';
                elseif($status == MRIM_ANKETA_INFO_STATUS_DBERR) $error = 'dberr';
                elseif($status == MRIM_ANKETA_INFO_STATUS_RATELIMERR) $error = 'limited';
                else $error = 'unknown';

                $data = array('type' => 'userinfo', 'id' => $seq, 'results' => $results, 'error' => $error);
            break;

            /**
            * Действие не определено
            */
            default:
                if($this->debug) $this->dump('[<<<] Unknown packet:', $headers.$data);

                $data = false;
            break;
        }

        return $data;
    }


    /**
    * Обрабатываем список контактов с сервера
    * 
    * @param string $data
    * @return void
    */
    private function parseContacts($data)
    {
        if(!$data) return false;

        $status        = $this->ul2int($data);
        $groups_num    = $this->ul2int($data);
        $group_mask    = $this->lps2str($data);
        $contacts_mask = $this->lps2str($data);

        // get groups
        for($i=0;$i<$groups_num;$i++)
        {
            $flag = $this->ul2int($data);
            $name = $this->unicode2win($this->lps2str($data));

            $this->groups[] = array('flag' => $flag, 'name' => $name, 'contacts_num' => 0, 'contacts' => array());
        }

        // get contacts
        while(strlen($data) > 0)
        {
            $unknow       = array();
            $flag         = $this->ul2int($data);
            $group        = $this->ul2int($data);
            $email        = $this->lps2str($data);
            $nick         = $this->unicode2win($this->lps2str($data));
            $sflag        = $this->ul2int($data);
            $status       = $this->ul2int($data);
            $unknow[]     = $this->lps2str($data);
            $status_uri   = $this->lps2str($data);
            $status_title = $this->unicode2win($this->lps2str($data));
            $status_descr = $this->unicode2win($this->lps2str($data));
            $unknow[]     = $this->ul2int($data);
            $client       = $this->lps2str($data);

            if( isset($this->statuses[ $status ]) )       $status = $this->statuses[ $status ];
            if( isset($this->substatuses[ $status_uri ]) ) $status_uri = $this->substatuses[ $status_uri ]['title'];

            $this->contacts[] = array(
                    'flag' => $flag,
                    'sflag' => $sflag,
                    'group' => $group,
                    'status' => $status,
                    'status_uri' => $status_uri,
                    'status_title' => $status_title,
                    'status_descr' => $status_descr,
                    'email' => $email,
                    'nick' => $nick,
                    'client' => $client,
                    'unknown' => $unknow
            );

            end($this->contacts);
            $id = key($this->contacts);

            $this->groups[$group]['contacts_num'] += 1;
            $this->groups[$group]['contacts'][] = array('id' => $id, 'email' => $email);
        }

        if($this->debug) {
            $tmp = array('status' => $status, 'groups_num' => $groups_num, 'group_mask' => $group_mask, 'contacts_mask' => $contacts_mask, 'gropus' => $this->groups, 'contacts' => $this->contacts);
            $this->dump('Contacts List:', var_export($tmp, true), false);
        }
    }


    /**
    * Читаем пакет с сокета
    * 
    * @access private
    * @return array
    */
    private function readSocket()
    {
        if($this->sock)
        {
            $packet = $headers = fread($this->sock, 44);
            if(!$packet) return false;

            $packet = unpack('L11', $packet);

            if($packet[5] != 0) $data = fread($this->sock, $packet[5]);
            else $data = '';

            $data = $this->parsePacket($packet[3], $packet[4], $data, $headers);

            return array('packet' => $packet, 'data' => $data);
        }

        return false;
    }


    /**
    * Записываем пакет в сокет
    * 
    * @access private
    * @param string $data
    * @param string $title
    * @return bool
    */
    private function writeSocket($data, $title = '')
    {
        if($this->sock)
        {
            if($title != '') $this->dump('[>>>] '.$title.':', $data);

            if( fwrite($this->sock, $data) ) return true;
            else return false;
        }

        return false;
    }


    /**
    * Подключаемся к серверу
    * 
    * @access public
    * @param string $login
    * @param string $password
    * @return bool
    */
    public function connect($login, $password)
    {
        if(!$this->checkEmail($login)) {
            $this->error = 'Error: Incorrect login (Email). Enter correct login (Email) and try again.';
            return false;
        }

        $this->sock = fsockopen('mrim.mail.ru', 2042, $errno, $errstr, 60);
        if(!$this->sock) $this->sock = fsockopen('mrim.mail.ru', 443, $errno, $errstr, 60);
        if(!$this->sock) {
            if(!$errstr) $this->error = 'Error: Can\'t connect to mrim.mail.ru!';
            else $this->error = 'Error: '.$errstr;
            return false;
        }

        $results = fread($this->sock, 20);
        if(!$results) {
            $this->error = 'Error: Can\'t get server and port.';
            return false;
        }

        if($this->debug) $this->dump('[<<<] Get Server:', $results, false);

        $this->disconnect();
        list($server, $port) = explode(':', $results);
        unset($results, $errno, $errstr);

        $this->sock = fsockopen($server, intval($port), $errno, $errstr, 60);
        if(!$this->sock) {
            if(!$errstr) $this->error = 'Error: Can\'t connect to server ('.$server.':'.$port.').';
            else $this->error = 'Error: '.$errstr.' ('.$server.':'.$port.').';
            return false;
        }

        stream_set_timeout($this->sock, 30);
        $this->writeSocket($this->packPacket(MRIM_CS_HELLO), 'MRIM_CS_HELLO');
        $this->readSocket();

        $status = array_search($this->substatuses[ $this->status_uri ]['status'], $this->statuses, true);
        $login_data = $this->str2lps($login) . $this->str2lps($password) . pack('L1', $status) . $this->str2lps($this->status_uri) .
                        $this->str2lps($this->win2unicode($this->status_title)) . $this->str2lps($this->win2unicode($this->status_descr)) .
                        $this->int2ul(0x03ff) . $this->str2lps($this->user_agent) . $this->str2lps('ru') . $this->str2lps($this->client_name.';');

        $this->writeSocket($this->packPacket(MRIM_CS_LOGIN, $login_data), 'MRIM_CS_LOGIN');
        $results = $this->readSocket();

        if(isset($results['data']['status']) && $results['data']['status'] == true)
        {
            socket_set_blocking($this->sock, 0);
            $this->writeSocket($this->packPacket(MRIM_CS_PING), 'MRIM_CS_PING');
            $this->ping['last'] = time();
            return true;
        }
        else
        {
            $this->disconnect();
            $this->error = 'Error: '.((isset($results['data']['reason']) && $results['data']['reason'] != '') ? $results['data']['reason'] : 'Can\'t login!');
            return false;
        }
    }


    /**
    * Отключаемся от сервера.
    * 
    * @access public
    * @return void
    */
    public function disconnect()
    {
        fclose($this->sock);
        $this->sock = false;
    }


    /**
    * Проверка соединения
    * 
    * @access public
    * @return bool
    */
    public function isConnected()
    {
        if(!$this->sock) return false;
        else return true;
    }


    /**
    * Пингуем сервер.
    * 
    * Если период пинга не прошел, то возвращается TRUE, иначе возвращается результат пинга,
    * т.е. если пинг успешен, то возвращается TRUE, иначе FALSE.
    * 
    * @access public
    * @return bool
    */
    public function pingServer()
    {
        if(!isset($this->ping['last']) || (isset($this->ping['last']) && (time()-$this->ping['last']) >= $this->ping['interval']))
        {
            if( !$this->writeSocket($this->packPacket(MRIM_CS_PING), 'MRIM_CS_PING') ) return false;
            $this->ping['last'] = time();
            return true;
        }

        return true;
    }


    /**
    * Получаем очередной пакет с сервера =)
    * 
    * @access public
    * @return array
    */
    public function readMessages()
    {
        $result = $this->readSocket();
        if(!$result || !$result['data']) return false;

        return $result['data'];
    }


    /**
    * Отправка сообщения
    * 
    * @access public
    * @param string $to
    * @param string $message
    * @return int
    */
    public function sendMessage($to, $message)
    {
        if(!$this->checkEmail($to)) return false;

        $packet = $this->int2ul(MESSAGE_FLAG_RTF) . $this->str2lps($to) . $this->str2lps($this->win2unicode($message)) .
                    $this->str2lps( $this->createRtfMessage( $message ) );
        $packet = $this->packPacket(MRIM_CS_MESSAGE, $packet);
        if( $this->writeSocket($packet, 'Send Message (to:'.$to.')') ) return $this->seq;
        else return false;
    }


    /**
    * Устанавливаем статус.
    * 
    * @access public
    * @param string $status_uri
    * @param string $status_title
    * @param string $status_desc
    * @return bool
    */
    public function setStatus($status_uri = '', $status_title = '', $status_descr = '')
    {
        $status_uri = $status_uri != '' ? strtolower($status_uri) : $this->status_uri;

        if( !isset($this->substatuses[ $status_uri ]) ) return false;
        if( strlen($status_title) > 16  ) $status_title = substr($status_title, 0, 13).'...';
        if( strlen($status_descr) > 255 ) $status_descr = substr($status_descr, 0, 252).'...';

        $status = array_search($this->substatuses[ $status_uri ]['status'], $this->statuses, true);
        $status_title = $this->win2unicode(($status_title != '' ? $status_title : $this->substatuses[$status_uri]['title']));
        $status_descr = $this->win2unicode(($status_descr != '' ? $status_descr : $this->status_descr));

        $packet = $this->int2ul($status) . $this->str2lps($status_uri) . $this->str2lps($status_title) . $this->str2lps($status_descr) . $this->int2ul(0x03ff);
        return $this->writeSocket($this->packPacket(MRIM_CS_CHANGE_STATUS, $packet), 'MRIM_CS_CHANGE_STATUS');
    }


    /**
    * Отправить уведомление о наборе сообщения
    * 
    * @access public
    * @param string $email
    * @return int
    */
    public function setTyping($email)
    {
        if(!$this->checkEmail($email)) return false;

        $packet = $this->int2ul(MESSAGE_FLAG_NOTIFY) . $this->str2lps($email) . $this->str2lps(' ') . $this->int2ul(0);
        $packet = $this->packPacket(MRIM_CS_MESSAGE, $packet);
        if( $this->writeSocket($packet, 'Set Typing (to:'.$email.')') ) return $this->seq;
        else return false;
    }


    /**
    * Получение анкеты пользователя
    * 
    * @access public
    * @param string $email
    * @return int
    */
    public function getUserInfo($email)
    {
        if(!$this->checkEmail($email)) return false;

        list($user, $domain) = explode('@', $email);
        $packet = $this->int2ul(0) . $this->str2lps($user) . $this->int2ul(1) . $this->str2lps($domain);
        $packet = $this->packPacket(MRIM_CS_WP_REQUEST, $packet);

        if( $this->writeSocket($packet, 'Search Contact ('.$email.')') ) return $this->seq;
        else return false;
    }
}

?>